﻿import ol_layer_Vector from 'ol/layer/Vector.js'
import {unByKey as ol_Observable_unByKey} from 'ol/Observable.js'
import {easeOut as ol_easing_easeOut} from 'ol/easing.js'
import ol_Object from 'ol/Object.js'
import ol_style_Style from 'ol/style/Style.js'
import ol_style_Stroke from 'ol/style/Stroke.js'
import ol_style_Fill from 'ol/style/Fill.js'
import {asString as ol_color_asString} from 'ol/color.js'
import { VERSION as ol_util_VERSION } from 'ol/util.js'

import {ol_coordinate_getIntersectionPoint} from '../geom/GeomUtils.js'

/** ol.layer.Vector.prototype.setRender3D
 * @extends {ol.layer.Vector}
 * @param {ol_render3D}
 */
ol_layer_Vector.prototype.setRender3D = function (r) {
  r.setLayer(this);
}

/** 
 * @classdesc
 *  3D vector layer rendering
 * @constructor
 * @param {Object} param
 *  @param {ol.layer.Vector} param.layer the layer to display in 3D
 *  @param {ol.style.Style} options.style drawing style
 *  @param {function|boolean} param.active a function that returns a boolean or a boolean ,default true
 *  @param {boolean} param.ghost use ghost style
 *  @param {number} param.maxResolution  max resolution to render 3D
 *  @param {number} param.defaultHeight default height if none is return by a propertie
 *  @param {function|string|Number} param.height a height function (returns height giving a feature) or a popertie name for the height or a fixed value
 */
var ol_render3D = class olrender3D extends ol_Object {
  constructor(options) {
    options = options || {}

    options.maxResolution = options.maxResolution || 100
    options.defaultHeight = options.defaultHeight || 0
    super(options)

    this.setStyle(options.style)
    this.set('ghost', options.ghost)
    this.setActive(options.active || options.active !== false)

    this.height_ = options.height = this.getHfn(options.height)
    if (options.layer)
      this.setLayer(options.layer)
  }
  /**
   * Set style associated with the renderer
   * @param {ol.style.Style} s
   */
  setStyle(s) {
    if (s instanceof ol_style_Style)
      this._style = s
    else
      this._style = new ol_style_Style()
    if (!this._style.getStroke()) {
      this._style.setStroke(new ol_style_Stroke({
        width: 1,
        color: 'red'
      }))
    }
    if (!this._style.getFill()) {
      this._style.setFill(new ol_style_Fill({ color: 'rgba(0,0,255,0.5)' }))
    }
    // Get the geometry
    if (s && s.getGeometry()) {
      var geom = s.getGeometry()
      if (typeof (geom) === 'function') {
        this.set('geometry', geom)
      } else {
        this.set('geometry', function () { return geom })
      }
    } else {
      this.set('geometry', function (f) { return f.getGeometry() })
    }
  }
  /**
   * Get style associated with the renderer
   * @return {ol.style.Style}
   */
  getStyle() {
    return this._style
  }
  /** Set active
   * @param {function|boolean} active
   */
  setActive(active) {
    if (typeof (active) === 'function') {
      this._active = active
    }
    else {
      this._active = function () { return active }
    }
    if (this.layer_)
      this.layer_.changed()
  }
  /** Get active
   * @return {boolean}
   */
  getActive() {
    return this._active()
  }
  /** Calculate 3D at potcompose
   * @private
   */
  onPostcompose_(e) {
    if (!this.getActive())
      return
    var res = e.frameState.viewState.resolution
    if (res > this.get('maxResolution'))
      return
    this.res_ = res * 400

    if (this.animate_) {
      var elapsed = e.frameState.time - this.animate_
      if (elapsed < this.animateDuration_) {
        this.elapsedRatio_ = this.easing_(elapsed / this.animateDuration_)
        // tell OL3 to continue postcompose animation
        e.frameState.animate = true
      } else {
        this.animate_ = false
        this.height_ = this.toHeight_
      }
    }

    var ratio = this._ratio = e.frameState.pixelRatio
    var ctx = e.context
    this.matrix_ = e.frameState.coordinateToPixelTransform
    this.inversePixelTransform_ = e.inversePixelTransform;
    // this.center_ = [ctx.canvas.width / 2 / ratio, ctx.canvas.height / ratio]
    this.center_ = [e.frameState.size[0] / 2, e.frameState.size[1]]

    var f = this.layer_.getSource().getFeaturesInExtent(e.frameState.extent)

    ctx.save()
    ctx.scale(ratio, ratio)
    var s = this.getStyle()
    ctx.lineWidth = s.getStroke().getWidth()
    ctx.strokeStyle = ol_color_asString(s.getStroke().getColor())
    ctx.fillStyle = ol_color_asString(s.getFill().getColor())
    var builds = []
    for (var i = 0; i < f.length; i++) {
      var h = this.getFeatureHeight(f[i])
      if (h) builds.push(this.getFeature3D_(f[i], h))
    }
    if (this.get('ghost')) {
      this.drawGhost3D_(ctx, builds)
    } else {
      this.drawFeature3D_(ctx, builds)
    }
    ctx.restore()
  }
  /** Set layer to render 3D
   */
  setLayer(l) {
    if (this._listener) {
      this._listener.forEach(function (l) {
        ol_Observable_unByKey(l)
      })
    }
    this.layer_ = l
    this._listener = l.on(['postcompose', 'postrender'], this.onPostcompose_.bind(this))
  }
  /** Create a function that return height of a feature
   *	@param {function|string|number} h a height function or a popertie name or a fixed value
   *	@return {function} function(f) return height of the feature f
   */
  getHfn(h) {
    switch (typeof (h)) {
      case 'function': return h
      case 'string': {
        var dh = this.get('defaultHeight')
        return (function (f) {
          return (Number(f.get(h)) || dh)
        })
      }
      case 'number': return (function ( /*f*/) { return h })
      default: return (function ( /*f*/) { return 10 })
    }
  }
  /** Animate rendering
   * @param {olx.render3D.animateOptions}
   *  @param {string|function|number} param.height an attribute name or a function returning height of a feature or a fixed value
   *  @param {number} param.duration the duration of the animatioin ms, default 1000
   *  @param {ol.easing} param.easing an ol easing function
   *	@api
   */
  animate(options) {
    options = options || {}
    this.toHeight_ = this.getHfn(options.height)
    this.animate_ = new Date().getTime()
    this.animateDuration_ = options.duration || 1000
    this.easing_ = options.easing || ol_easing_easeOut
    // Force redraw
    this.layer_.changed()
  }
  /** Check if animation is on
   *	@return {bool}
   */
  animating() {
    if (this.animate_ && new Date().getTime() - this.animate_ > this.animateDuration_) {
      this.animate_ = false
    }
    return !!this.animate_
  }
  /** Get feature height
   * @param {ol.Feature} f
   */
  getFeatureHeight(f) {
    if (this.animate_) {
      var h1 = this.height_(f)
      var h2 = this.toHeight_(f)
      return (h1 * (1 - this.elapsedRatio_) + this.elapsedRatio_ * h2)
    }
    else
      return this.height_(f)
  }
  /** Get elevation line
   * @private
   */
  hvector_(pt, h) {
    var p0 = [
      pt[0] * this.matrix_[0] + pt[1] * this.matrix_[1] + this.matrix_[4],
      pt[0] * this.matrix_[2] + pt[1] * this.matrix_[3] + this.matrix_[5]
    ]
    var p1 = [
      p0[0] + h / this.res_ * (p0[0] - this.center_[0]),
      p0[1] + h / this.res_ * (p0[1] - this.center_[1])
    ]

    var version = parseFloat(ol_util_VERSION);
    // ol@v9.1+
    if (version > 9.0) {
      p0 = [
        p0[0] * this.inversePixelTransform_[0] - p0[1] * this.inversePixelTransform_[1] + this.inversePixelTransform_[4],
        - p0[0] * this.inversePixelTransform_[2] + p0[1] * this.inversePixelTransform_[3] + this.inversePixelTransform_[5]
      ]
      p1 = [
        p1[0] * this.inversePixelTransform_[0] - p1[1] * this.inversePixelTransform_[1] + this.inversePixelTransform_[4],
        - p1[0] * this.inversePixelTransform_[2] + p1[1] * this.inversePixelTransform_[3] + this.inversePixelTransform_[5]
      ]
      return {
        p0: [p0[0]/this._ratio, p0[1]/this._ratio],
        p1: [p1[0]/this._ratio, p1[1]/this._ratio]
      }
    }
    // Old versions
    return {
      p0: p0,
      p1: p1
    }
  }
  /** Get drawing
   * @private
   */
  getFeature3D_(f, h) {
    var geom = this.get('geometry')(f)
    var c = geom.getCoordinates()
    switch (geom.getType()) {
      case "Polygon":
        c = [c]
      // fallthrough
      case "MultiPolygon":
        var build = []
        for (var i = 0; i < c.length; i++) {
          for (var j = 0; j < c[i].length; j++) {
            var b = []
            for (var k = 0; k < c[i][j].length; k++) {
              b.push(this.hvector_(c[i][j][k], h))
            }
            build.push(b)
          }
        }
        return { type: "MultiPolygon", feature: f, geom: build, height: h }
      case "Point":
        return { type: "Point", feature: f, geom: this.hvector_(c, h), height: h }
      default: return {}
    }
  }
  /** Draw a feature
   * @param {CanvasRenderingContext2D} ctx
   * @param {ol.Feature} build
   * @private
   */
  drawFeature3D_(ctx, build) {
    var i, j, b, k
    // Construct
    for (i = 0; i < build.length; i++) {
      switch (build[i].type) {
        case "MultiPolygon": {
          for (j = 0; j < build[i].geom.length; j++) {
            b = build[i].geom[j]
            for (k = 0; k < b.length; k++) {
              ctx.beginPath()
              ctx.moveTo(b[k].p0[0], b[k].p0[1])
              ctx.lineTo(b[k].p1[0], b[k].p1[1])
              ctx.stroke()
            }
          }
          break
        }
        case "Point": {
          var g = build[i].geom
          ctx.beginPath()
          ctx.moveTo(g.p0[0], g.p0[1])
          ctx.lineTo(g.p1[0], g.p1[1])
          ctx.stroke()
          break
        }
        default: break
      }
    }
    // Roof
    for (i = 0; i < build.length; i++) {
      switch (build[i].type) {
        case "MultiPolygon": {
          ctx.beginPath()
          for (j = 0; j < build[i].geom.length; j++) {
            b = build[i].geom[j]
            if (j == 0) {
              ctx.moveTo(b[0].p1[0], b[0].p1[1])
              for (k = 1; k < b.length; k++) {
                ctx.lineTo(b[k].p1[0], b[k].p1[1])
              }
            } else {
              ctx.moveTo(b[0].p1[0], b[0].p1[1])
              for (k = b.length - 2; k >= 0; k--) {
                ctx.lineTo(b[k].p1[0], b[k].p1[1])
              }
            }
            ctx.closePath()
          }
          ctx.fill("evenodd")
          ctx.stroke()
          break
        }
        case "Point": {
          b = build[i]
          var t = b.feature.get('label')
          if (t) {
            var p = b.geom.p1
            var f = ctx.fillStyle
            ctx.fillStyle = ctx.strokeStyle
            ctx.textAlign = 'center'
            ctx.textBaseline = 'bottom'
            ctx.fillText(t, p[0], p[1])
            var m = ctx.measureText(t)
            var h = Number(ctx.font.match(/\d+(\.\d+)?/g).join([]))
            ctx.fillStyle = "rgba(255,255,255,0.5)"
            ctx.fillRect(p[0] - m.width / 2 - 5, p[1] - h - 5, m.width + 10, h + 10)
            ctx.strokeRect(p[0] - m.width / 2 - 5, p[1] - h - 5, m.width + 10, h + 10)
            ctx.fillStyle = f
            //console.log(build[i].feature.getProperties())
          }
          break
        }
        default: break
      }
    }
  }
  /**
   * @private
   */
  drawGhost3D_(ctx, build) {
    var i, j, b, k
    // Construct
    for (i = 0; i < build.length; i++) {
      switch (build[i].type) {
        case "MultiPolygon": {
          for (j = 0; j < build[i].geom.length; j++) {
            b = build[i].geom[j]
            for (k = 0; k < b.length - 1; k++) {
              ctx.beginPath()
              ctx.moveTo(b[k].p0[0], b[k].p0[1])
              ctx.lineTo(b[k].p1[0], b[k].p1[1])
              ctx.lineTo(b[k + 1].p1[0], b[k + 1].p1[1])
              ctx.lineTo(b[k + 1].p0[0], b[k + 1].p0[1])
              ctx.lineTo(b[k].p0[0], b[k].p0[1])

              var m = [(b[k].p0[0] + b[k + 1].p0[0]) / 2, (b[k].p0[1] + b[k + 1].p0[1]) / 2]
              var h = [b[k].p0[1] - b[k + 1].p0[1], -b[k].p0[0] + b[k + 1].p0[0]]
              var c = ol_coordinate_getIntersectionPoint(
                [m, [m[0] + h[0], m[1] + h[1]]],
                [b[k].p1, b[k + 1].p1]
              )
              var gradient = ctx.createLinearGradient(
                m[0], m[1],
                c[0], c[1]
              )
              gradient.addColorStop(0, 'rgba(255,255,255,.2)')
              gradient.addColorStop(1, 'rgba(255,255,255,0)')
              ctx.fillStyle = gradient
              ctx.fill()
            }
          }
          break
        }
        case "Point": {
          var g = build[i].geom
          ctx.beginPath()
          ctx.moveTo(g.p0[0], g.p0[1])
          ctx.lineTo(g.p1[0], g.p1[1])
          ctx.stroke()
          break
        }
        default: break
      }
    }
  }
}

export default ol_render3D
